﻿using S7.Net;
using S7.Net.Types;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Zhaoxi.FactoryDeviceSystem.Exceptions;
using Zhaoxi.FactoryDeviceSystem.Utils;
using static Zhaoxi.FactoryDeviceSystem.Utils.CommonHelper;

namespace Zhaoxi.FactoryDeviceSystem.Service
{
    public class PlcService : IDisposable
    {
        public void Dispose()
        {
            throw new NotImplementedException();
        }

        Plc plc = null;
        int MaxRetries = 3;

        /// <summary>
        /// 封装安全的Write方法
        /// 该方法用于安全地向PLC（可编程逻辑控制器）写入数据，当写入失败时会进行重试操作
        /// </summary>
        private bool SafeWrite(string address, bool state)
        {
            // 初始化重试计数器，记录当前重试的次数
            int retryCount = 0;
            // 进入循环，只要重试次数小于最大重试次数，就会继续尝试写入操作
            while (retryCount < MaxRetries)
            {
                try
                {
                    byte[] data = ConvertToBytes(address, state);
                    AddCrc(ref data);
                    plc.Write(address, state);
                    return true;
                }
                //添加自动丢弃
                catch (CrcValidationException ex)
                {
                    // 校验失败，丢弃错误包，继续重试
                    if (++retryCount > MaxRetries)
                    {
                        // 可以根据地址前缀判断
                        string addressType = GetAddressType(address);
                        throw new CrcValidationException($"写入{addressType}{address}失败，CRC校验多次失败", ex);
                    }
                    // 线程休眠50毫秒，给PLC一些时间恢复
                    System.Threading.Thread.Sleep(50);
                    // 调用重新连接方法，尝试重新连接到PLC
                    Reconnect();
                }
                catch (PlcException ex)
                {
                    retryCount++;
                    // 检查重试次数是否已经达到或超过最大重试次数
                    if (retryCount >= MaxRetries)
                    {
                        string addressType = GetAddressType(address);
                        throw new PlcCommunicationException($"写入{addressType}{address}失败", ex);
                        return false;
                    }
                    // 如果还未达到最大重试次数，线程暂停1秒（1000毫秒），给PLC和网络一些时间恢复，然后进行下一次重试
                    Thread.Sleep(1000);
                }
            }
            return false;
        }

        /// <summary>
        /// 新增批量安全写入方法
        /// </summary>
        /// <returns></returns>
        public bool SafeWriteBulk(DataItem[] dataItems)
        {
            int retryCount = 0;
            while (retryCount < MaxRetries)
            {
                try
                {
                    // 对每个 DataItem 处理数据并附加 CRC
                    foreach (var item in dataItems)
                    {
                        // 将 DataItem 的值转换为字节数组
                        byte[] data = SerializeDataItem(item);

                        // 附加 CRC 校验码
                        AddCrc(ref data);

                        // 更新 DataItem 的值为带 CRC 的字节流
                        item.Value = data;
                    }
                    plc.Write(dataItems);
                    return true;
                }
                catch (PlcException ex)
                {
                    retryCount++;
                    if (retryCount >= MaxRetries)
                    {
                        Console.WriteLine($"批量写入失败: {ex.Message}");
                        return false;
                    }
                    Thread.Sleep(1000);
                }
            }
            return false;
        }

        /// <summary>
        /// 定义一个方法，用于验证接收到的数据的 CRC 校验码
        /// </summary>
        /// <param name="data"></param>
        /// <exception cref="PlcCommunicationException"></exception>
        private void ValidateCrc(byte[] data)
        {
            // 检查数据长度是否足够包含 CRC 校验码（CRC 校验码通常为 2 字节）
            if (data.Length < 2)
            {
                // 如果数据长度不足，抛出自定义的 PLC 通信异常
                throw new CrcValidationException("数据长度不足以包含 CRC 校验码");
            }

            // 提取数据和 CRC 校验码
            // 创建一个新的字节数组，用于存储去除 CRC 校验码后的数据
            byte[] payload = new byte[data.Length - 2];
            // 将 data 数组中从索引 0 开始的长度为 payload.Length 的数据复制到 payload 数组中
            Array.Copy(data, 0, payload, 0, payload.Length);
            // 提取接收到的 CRC 校验码，存储在一个 2 字节的数组中
            // 倒数第一位、倒数第二位即最后两位才是CRC校验码
            byte[] receivedCrcBytes = new byte[2] { data[data.Length - 2], data[data.Length - 1] };

            // 计算实际 CRC
            // 调用 Crc16 类的 Compute 方法，计算 payload 数组的 CRC 校验值
            ushort calculatedCrc = Crc16.Compute(payload);
            // 调用 Crc16 类的 ToBigEndianBytes 方法，将计算得到的 CRC 校验值转换为大端序字节数组
            byte[] calculatedCrcBytes = Crc16.ToBigEndianBytes(calculatedCrc);

            // 校验
            // 比较计算得到的 CRC 字节数组和接收到的 CRC 字节数组是否相等
            if (!calculatedCrcBytes.SequenceEqual(receivedCrcBytes))
            {
                // 如果不相等，抛出自定义的 PLC 通信异常，表示 CRC 校验失败
                throw new CrcValidationException("CRC 校验失败");
            }
        }

        /// <summary>
        /// 定义一个方法，用于在数据末尾添加 CRC 校验码
        /// </summary>
        /// <param name="data"></param>
        private void AddCrc(ref byte[] data)
        {
            // 计算 CRC 并附加到数据末尾
            // 调用 Crc16 类的 Compute 方法，计算 data 数组的 CRC 校验值
            ushort crc = Crc16.Compute(data);
            // 调用 Crc16 类的 ToBigEndianBytes 方法，将计算得到的 CRC 校验值转换为大端序字节数组
            // 放不下所以需要转换大端（只能存下8位）
            byte[] crcBytes = Crc16.ToBigEndianBytes(crc);
            // 调整 data 数组的大小，增加 2 个字节以容纳 CRC 校验码
            Array.Resize(ref data, data.Length + 2);
            // 将计算得到的 CRC 字节数组复制到 data 数组的末尾
            Array.Copy(crcBytes, 0, data, data.Length - 2, 2);
        }
        /// <summary>
        /// 重新连接到PLC的方法
        /// </summary>
        private void Reconnect()
        {
            try
            {
                // 关闭与PLC的连接
                plc.Close();
            }
            catch
            {
                // 忽略关闭连接时可能出现的异常
            }
            // 线程休眠100毫秒，给PLC一些时间处理关闭操作
            System.Threading.Thread.Sleep(100);
            // 重新打开与PLC的连接
            plc.Open();
        }

        /// <summary>
        /// 根据地址的前缀来判断地址类型
        /// </summary>
        /// <param name="address"></param>
        /// <returns></returns>
        private string GetAddressType(string address)
        {
            if (address.StartsWith("I", StringComparison.OrdinalIgnoreCase))
            {
                return "输入（I）";
            }
            else if (address.StartsWith("Q", StringComparison.OrdinalIgnoreCase))
            {
                return "输出（Q）";
            }
            else if (address.StartsWith("DB", StringComparison.OrdinalIgnoreCase))
            {
                return "数据块（DB）";
            }
            return "未知类型";
        }
        /// <summary>
        /// 校验bytes参数转换
        /// </summary>
        private byte[] ConvertToBytes(string address, bool state)
        {
            // 根据协议将地址和状态转换为字节数组
            // 示例伪代码：
            byte[] addrBytes = Encoding.ASCII.GetBytes(address);
            byte[] valueBytes = BitConverter.GetBytes(state);
            byte[] data = new byte[addrBytes.Length + valueBytes.Length];
            Array.Copy(addrBytes, 0, data, 0, addrBytes.Length);
            Array.Copy(valueBytes, 0, data, addrBytes.Length, valueBytes.Length);
            return data;
        }

        /// <summary>
        /// 使用 S7.net 内置方法将 DataItem 的值序列化为字节数组
        /// </summary>
        public static byte[] SerializeDataItem(DataItem item)
        {
            switch (item.DataType)
            {
                case DataType.DataBlock:
                    return S7.Net.Types.Byte.ToByteArray((byte)item.Value);
                case DataType.Timer:
                case DataType.Counter:
                    return S7.Net.Types.Word.ToByteArray((ushort)item.Value);
                case DataType.Input:
                case DataType.Output:
                case DataType.Memory:
                    //return S7.Net.Types.Bit.ToByteArray((bool)item.Value);
                    // 自定义将布尔值转换为字节数组的逻辑
                    bool boolValue = (bool)item.Value;
                    return new byte[] { (byte)(boolValue ? 1 : 0) };
                default:
                    throw new NotSupportedException($"不支持的 DataType: {item.DataType}");
            }
        }
    }
}
